package pathfinding

import (
	"sync"

	"gddgame.cc/galaxy/utils"
)

type DijkstraNode interface {
	utils.EqualInterface

	Can(mask PathMask) bool
	GetId() interface{}
	GetEdges(endLink map[interface{}]DijkstraEdge, mask PathMask) []DijkstraEdge
	Distance(node DijkstraNode) float64
}

type DijkstraEdge interface {
	GetCost() float64
	Connect() DijkstraNode
	Switch(node DijkstraNode)
}

type DijkstraLogic interface {
	GetCost(node *VisitedNode, end *VisitedNode) int
	EndEdge(end *VisitedNode, mask PathMask) map[interface{}]DijkstraEdge
}

type BaseLogic struct {
}

func (logic *BaseLogic) GetCost(node *VisitedNode, end *VisitedNode) int {
	return utils.ToInt(node.MinCostToStart)
}

func (logic *BaseLogic) EndEdge(end *VisitedNode, mask PathMask) map[interface{}]DijkstraEdge {
	endLink := map[interface{}]DijkstraEdge{}
	edges := end.DijkstraNode.GetEdges(endLink, mask)
	if len(edges) > 0 {
		for index, cnn := range edges {
			endLink[cnn.Connect().GetId()] = edges[index]
			edges[index].Switch(end.DijkstraNode)
		}
	}
	return endLink
}

type AStarLogic struct {
}

func (logic *AStarLogic) GetCost(node *VisitedNode, end *VisitedNode) int {
	return utils.ToInt(node.MinCostToStart + node.DijkstraNode.Distance(end.DijkstraNode))
}

func (logic *AStarLogic) EndEdge(end *VisitedNode, mask PathMask) map[interface{}]DijkstraEdge {
	endLink := map[interface{}]DijkstraEdge{}
	edges := end.DijkstraNode.GetEdges(endLink, mask)
	if len(edges) > 0 {
		for index, cnn := range edges {
			endLink[cnn.Connect().GetId()] = edges[index]
			edges[index].Switch(end.DijkstraNode)
		}
	}
	return endLink
}

type RouteLogic struct {
}

func (logic *RouteLogic) GetCost(node *VisitedNode, end *VisitedNode) int {
	distance := node.DijkstraNode.Distance(end.DijkstraNode)
	return utils.ToInt(distance*100/(node.MinCostToStart+distance) + distance)
}

func (logic *RouteLogic) EndEdge(end *VisitedNode, mask PathMask) map[interface{}]DijkstraEdge {
	return map[interface{}]DijkstraEdge{}
}

type VisitedNode struct {
	DijkstraNode   DijkstraNode
	MinCostToStart float64
	NearestToStart *VisitedNode
	Visited        bool
	Cost           float64
}

func (node *VisitedNode) Equal(other interface{}) bool {
	o := other.(*VisitedNode)
	return node.DijkstraNode.Equal(o.DijkstraNode)
}

type Dijkstra struct {
	nodePool  sync.Pool
	queuePool sync.Pool
}

var (
	BaseLogicInf  = &BaseLogic{}
	AStarLogicInf = &AStarLogic{}
	RouteLogicInf = &RouteLogic{}
)

func NewDijkstra() *Dijkstra {
	d := &Dijkstra{
		nodePool: sync.Pool{New: func() interface{} {
			return &VisitedNode{}
		}},
		queuePool: sync.Pool{New: func() interface{} {
			return utils.NewNodeQueue()
		}},
	}
	return d
}

func (search *Dijkstra) buildShortestPath(shortestPath *[]DijkstraNode, endNode *VisitedNode) float64 {
	node := endNode
	cost := float64(0)
	for {
		if node.NearestToStart == nil {
			break
		}
		if node.NearestToStart.DijkstraNode.GetId() == node.DijkstraNode.GetId() {
			//fmt.Println(node.DijkstraNode.GetId())
			break
		}
		*shortestPath = append(*shortestPath, node.NearestToStart.DijkstraNode)
		cost += node.Cost
		tmp := node.NearestToStart
		search.nodePool.Put(node)
		node = tmp
	}
	return cost
}

func (search *Dijkstra) ToDijkstra(startNode DijkstraNode, endNode DijkstraNode, mask PathMask, shortestPath *[]DijkstraNode, logic DijkstraLogic) (bool, float64) {
	start := search.nodePool.Get().(*VisitedNode)
	start.DijkstraNode = startNode
	start.MinCostToStart = -1
	start.Visited = false
	start.NearestToStart = nil

	end := search.nodePool.Get().(*VisitedNode)
	end.DijkstraNode = endNode
	end.MinCostToStart = -1
	end.Visited = false
	end.NearestToStart = nil
	r := search.DijkstraSearch(start, end, mask, logic)
	cost := float64(0)

	if r {
		*shortestPath = append(*shortestPath, endNode)
		cost = search.buildShortestPath(shortestPath, end)
		for i := 0; i < len(*shortestPath)/2; i++ {
			j := len(*shortestPath) - i - 1
			(*shortestPath)[i], (*shortestPath)[j] = (*shortestPath)[j], (*shortestPath)[i]
		}
		//fmt.Println(2)
	}
	return r, cost
}

func (search *Dijkstra) DijkstraSearch(start *VisitedNode, end *VisitedNode, mask PathMask, logic DijkstraLogic) bool {
	prioQueue := search.queuePool.Get().(*utils.PriorityNodeQueue)
	prioQueue.Push(start, 0)
	visited := map[interface{}]*VisitedNode{}
	visited[start.DijkstraNode.GetId()] = start
	visited[end.DijkstraNode.GetId()] = end
	endLink := logic.EndEdge(end, mask)

	r := false
	for {
		n, _ := prioQueue.Pop()
		node := n.(*VisitedNode)
		//fmt.Println("node<-", node.DijkstraNode.GetId(), v)
		if node.Equal(end) {
			r = true
			prioQueue.Clean()
			break
		}
		edges := node.DijkstraNode.GetEdges(endLink, mask)
		for _, cnn := range edges {
			childNode := cnn.Connect()
			if !childNode.Can(mask) {
				continue
			}
			var visitedNode *VisitedNode
			if vn, ok := visited[childNode.GetId()]; ok {
				if vn.Visited {
					//fmt.Println("visited", childNode.GetId())
					continue
				}
				visitedNode = vn
			}
			if visitedNode == nil {
				visitedNode = search.nodePool.Get().(*VisitedNode)
				visitedNode.Visited = false
				visitedNode.NearestToStart = nil
				visitedNode.MinCostToStart = -1
				visitedNode.DijkstraNode = childNode
				visited[childNode.GetId()] = visitedNode
			}
			cost := cnn.GetCost()
			if visitedNode.MinCostToStart < 0 || node.MinCostToStart+cost < visitedNode.MinCostToStart {
				visitedNode.MinCostToStart = node.MinCostToStart + cost
				visitedNode.NearestToStart = node
				visitedNode.Cost = cost
				//fmt.Println("cost->", childNode.GetId(), cost, visitedNode.MinCostToStart, logic.GetCost(visitedNode, end))
				if prioQueue.Contains(visitedNode) < 0 {
					prioQueue.Push(visitedNode, logic.GetCost(visitedNode, end))
				} else {
					prioQueue.Fix(visitedNode, logic.GetCost(visitedNode, end))
				}
			}
		}
		visited[node.DijkstraNode.GetId()].Visited = true
		if prioQueue.Len() == 0 {
			break
		}
	}
	//fmt.Println(r)
	search.queuePool.Put(prioQueue)
	return r
}
